Implementazione della libreria PW.JSON in VB.NET

PW.JSON Library Implementation in VB.NET (english version)

In questo articolo spiegherò come ho realizzato la libreria PW.JSON.dll che consente di gestire Stringhe e Oggetti secondo lo standard definito dal sito www.json.org.

Come si può leggere dal sito menzionato JSON è un linguaggio per rappresentare sottoforma di stringhe gli oggetti. Questo risulta molto utile quando si vuole per esempio scambiare dati complessi (oggetti) attraverso linguaggi differenti come ad esempio ASP.NET e Javascript o ActionScript e magari attraverso un protocollo che gestisce solo testo come l'HTTP.

Ho curiosato tra le librerie presenti nei link del sito www.json.org e ho visto che non ci sono molti esempi in VB.NET. Si trova una versione per VB 6, una per C# molto completa ma anche molto complessa, così alla fine ho trovato qualcosa di utile in questo sito http://mikeoncode.blogspot.com/2007/05/json-re-visited-from.html.

A questo codice che permette di generare un oggetto specifico per poi ottenere la corrispondente stringa JSON ho apportato alcune modifiche tra le quali ad esempio la possibilità di gestire le proprietà nulle (Nothing -> null) e la convenzione dei doppi apici (") anziché quelli singoli (') per la definizione dei nomi delle proprietà e dei corrispondenti valori stringa (così come espressamente indicato dalla definizione dello standard JSON).

Il risultato è il codice seguente:

Imports System.Text

Friend Class NetJSON

#Region " Private Declarations"
Dim myName As String = ""
Dim myContent As New Hashtable
Const dblQuote As Char = Chr(34)
#End Region

#Region
" Enumerations"
Private Enum dataType
    dt_Nothing
    dt_Boolean
    dt_Decimal
    dt_Double
    dt_Integer
    dt_string
    dt_Array
    dt_NetJSON
End Enum
#End
Region

#Region
" Public Constructors, Methods and Properties"
Public Sub New()

End Sub

Public Sub New(ByVal nameString As String)
    myName = nameString
End Sub

Public Sub AddNameValue(ByVal nameString As String, ByVal Value As Object)
    StoreValue(nameString, Value)
End Sub

Public Overrides Function toString() As String
    Dim
Resp As New StringBuilder
   
Dim Firstcall As Boolean = True
    Dim
TailBrace As String = "}"

    If myName.Length > 0 Then
        Resp.Append("{" & dblQuote & myName & dblQuote & ": {")
        TailBrace &=
"}"
   
Else
        Resp.Append("{")
   
End If

    Dim myEnumerator As IDictionaryEnumerator = myContent.GetEnumerator()
   
While myEnumerator.MoveNext
        Resp.Append(IIf(Firstcall,
"", ", ") & dblQuote & myEnumerator.Key & dblQuote & ": " & MakeString(myEnumerator.Value))
        Firstcall =
False
    End
While

    Resp.Append(TailBrace)
   
Return Resp.ToString()
End Function

#End
Region

#Region
" Private Functions and Subroutines"
Private Function MakeString(ByVal ThisData As Object) As String
    Dim
ThisType As dataType = GetDataType(ThisData)

   
If ThisType = dataType.dt_Array Then
        Dim
TestArray(ThisData.length) As Object
        Dim
aLoop As Int16
       
Dim ArrayStruct As New StringBuilder("[")
       
Dim FirstCall As Boolean = True

        ThisType = GetDataType(ThisData(0))

       
For aLoop = 0 To ThisData.Length - 1
            ArrayStruct.Append(IIf(FirstCall,
"", ", ") & MakeElementString(ThisData(aLoop), ThisType))
            FirstCall =
False
        Next

        ArrayStruct.Append(
"]")
       
Return ArrayStruct.ToString()
   
Else
        Return
MakeElementString(ThisData, ThisType)
   
End If
End
Function

Private Function MakeElementString(ByVal ThisData As Object, ByVal ThisDataType As dataType)
   
Select Case ThisDataType
       
Case dataType.dt_Boolean
           
Return IIf(CBool(ThisData), "true", "false")
       
Case dataType.dt_Decimal
           
Return String.Format("{0}", ThisData).Replace(","c, "."c)
       
Case dataType.dt_string
           
Return Chr(34) & CType(ThisData, String).Replace("\", "\\").Replace("/", "\/").Replace(vbCrLf, "\n").Replace(vbTab, "\t").Replace(Chr(34), "\" & Chr(34)) & Chr(34)
       
Case dataType.dt_Nothing
           
Return "null"
        Case dataType.dt_NetJSON
           
Return ThisData.ToString()
       
Case Else
            Return
""
    End Select
End
Function

Private Function GetDataType(ByVal Value As Object) As dataType
   
If TypeOf Value Is Array Then
        Return
dataType.dt_Array
   
ElseIf TypeOf Value Is Single Or TypeOf Value Is Double Then
        Return
dataType.dt_Double
   
ElseIf TypeOf Value Is Decimal Then
        Return
dataType.dt_Decimal
   
ElseIf TypeOf Value Is Boolean Then
        Return
dataType.dt_Boolean
   
ElseIf TypeOf Value Is String Then
        Return
dataType.dt_string
   
ElseIf TypeOf Value Is Integer Or TypeOf Value Is Int16 Or TypeOf Value Is Int32 Or TypeOf Value Is Int64 Then
        Return
dataType.dt_Integer
   
ElseIf TypeOf Value Is NetJSON Then
        Return
dataType.dt_NetJSON
   
ElseIf Value Is Nothing Then
        Return
dataType.dt_Nothing
   
End If
End
Function


Private Sub StoreValue(ByVal nameString As String, ByVal Value As Object)
   
Select Case GetDataType(Value)
       
Case dataType.dt_Array
           
Dim copyArray(Value.length - 1) As Object
            For
aLoop As Int16 = 0 To Value.length - 1
                copyArray(aLoop) = Value(aLoop)
           
Next
            myContent.Add(nameString, copyArray)

       
Case dataType.dt_Boolean
           
Dim wrkBoolean As Boolean = CBool(Value)
            myContent.Add(nameString, wrkBoolean)

        Case dataType.dt_Double, dataType.dt_Integer, dataType.dt_Decimal
           
Dim wrkDecimal As Decimal = CDec(Value)
            myContent.Add(nameString, wrkDecimal)

       
Case dataType.dt_string
           
Dim wrkString As String = CStr(Value)
            myContent.Add(nameString, wrkString)

        
Case dataType.dt_NetJSON
            myContent.Add(nameString, Value)

        
Case dataType.dt_Nothing
            myContent.Add(nameString,
Nothing)

   
End Select
End
Sub

#End
Region

End
Class

Con la classe sopra descritta che è sostanzialmente un perfezionamento della classe scritta da Mike Griffiths, posso generare un oggetto di tipo NetJSON al quale aggiungo di volta in volta le proprietà che mi interessano. Notate che nel caso in cui si debba aggiungere una proprità di tipo complesso (un oggetto), tale proprietà deve essere comunque passata come tipo NetJSON in quanto è l'unico tipo di oggetto riconoscito dall'algoritmo.

Il vantaggio però che vorrei ottenere dall'uso di JSON è quello di poter trasformare qualsiasi oggetto in una stringa JSON. Per farlo ho costruito una classe JSONHelper con un metodo pubblico statico che, attraverso "reflection" converte un qualsiasi oggetto in una stringa JSON.

Ecco il codice della classe JSON, il metodo in questione è ObjectToString:

Imports System.Reflection

Public Class JSONHelper

#Region "Gestione Object via Reflection"

Public Shared Function ObjectToString(ByVal Obj As Object) As String
    If Obj Is Nothing Then Return ""
    Dim _t As Type = Obj.GetType()
    Return ConvertSubObjectToNetJSON("", Obj).toString
End Function

Private Shared Function ConvertSubObjectToNetJSON(ByVal Name As String, ByRef Obj As Object) As NetJSON
    If Obj Is Nothing Then Return Nothing
    Dim _t As Type = Obj.GetType()
    Dim result As New NetJSON(Name)
    For Each _p As PropertyInfo In _t.GetProperties()
        If _p.PropertyType.IsPrimitive Then
           result.AddNameValue(_p.Name, _p.GetValue(Obj, Nothing))
        ElseIf _p.PropertyType.IsArray Then
           result.AddNameValue(_p.Name, _p.GetValue(Obj, Nothing))
        ElseIf _p.PropertyType.IsClass AndAlso _p.PropertyType.Name <> GetType(String).Name Then
           result.AddNameValue(_p.Name, ConvertSubObjectToNetJSON("", _p.GetValue(Obj, Nothing)))
        ElseIf _p.PropertyType.Name = GetType(String).Name Then
           result.AddNameValue(_p.Name, _p.GetValue(Obj, Nothing))
        Else
           ThrowNew NotImplementedException("Property Type '" & _p.PropertyType.Name & "' not yet implemented")
        End If
    Next
    Return result
End Function

Public Shared Function StringToObject(ByVal JSONString As String, ByVal ClassType As Type) As Object
    Dim ojson As New JSON.JSONObject
    Return ojson.parse(JSONString, ClassType)
End Function

#End Region

End Class

 

Come si vede dal codice del metodo ObjectToString ed in particolare nel metodo privato ConvertSubObjectToNetJSON, si procede con l'esaminare tutte le proprietà dell'oggetto da convertire, utilizzando la classe PropertyInfo della libreria System.Reflection. Per ogni proprietà si verifica se si tratta di un tipo primitivo (Integer, Double, Single, ecc...), di un Array o di una classe (Stringa o altro) e si provvede ad aggiungere l'opportuno valore all'oggetto di appoggio NetJSON mediate il metodo AddNameValue.

Notare che per esempio per il tipo Date verrebbe rilasciata un eccezione di tipo NotImplementedException in quanto lo stesso linguaggio JSON non prevede il tipo data.

Vediamo ora un semplice esempio di utilizzo del metodo ObjectToString fin qui descritto:

Imports PW.JSON

Module Module1

  Class Prova

    Private _id As Integer
    Private _name As String
    Private _valido As Boolean
    Private _subObject As Prova
    Private _numero As Integer
    Private _numeroDec As Double
    Private _array() As String

    Public Property ID() As Integer
      Get
        Return _id
      End Get
      Set(ByVal value As Integer)
        _id = value
      End Set
    End Property

    Public Property Name() As String
      Get
        Return _name
      End Get
      Set(ByVal value As String)
        _name = value
      End Set
    End Property

    Public Property Valido() As Boolean
      Get
        Return _valido
      End Get
      Set(ByVal value As Boolean)
        _valido = value
      End Set
    End Property

    Public Property SubObject() As Prova
      Get
        Return _subObject
      End Get
      Set(ByVal value As Prova)
        _subObject = value
      End Set
    End Property

    Public Property NumeroDec() As Double
      Get
        Return _numeroDec
      End Get
      Set(ByVal value As Double)
        _numeroDec = value
      End Set
    End Property

    Public Property Array() As String()
      Get
        Return _array
      End Get
      Set(ByVal value As String())
        _array = value
      End Set
    End Property

    Public Sub New(ByVal ID As Integer, ByVal Name As String)
      _id = ID
      _name = Name
    End Sub

    Public Function SomeMethod() As String
      Return "Method: " & _id
    End Function

  End Class

Sub Main()
    Dim objprova As New Prova(1, "Nome Object")
    objprova.Array = Split(
"A E I O U")
    objprova.NumeroDec = 100.34
    objprova.SubObject =
New Prova(2, "Nome - SubObject")
    objprova.Valido =
True
    Console.WriteLine(PW.JSON.JSONHelper.ObjectToString(objprova))
    Console.ReadLine()
 
End Sub
End Module


 

Il risultato che si ottiene in output è la seguente stringa JSON:

{"NumeroDec": 100.34, "Name": "Nome Object", "Array": ["A", "E", "I", "O", "U"], "SubObject": {"NumeroDec": 0, "Name": "Nome - SubObject", "Array": null, "SubObject": null, "Valido": false, "ID": 2}, "Valido": true, "ID": 1}

Ora vediamo invece il metodo inverso, ovvero creare un oggetto dalla stringa JSON, utilizzando la stringa prodotta dall'output precedente:

Sub Main()

Dim strJSON As String = "{""NumeroDec"": 100.34, ""Name"": ""Nome Object"", " & _
                        " ""Array"": [""A"", ""E"", ""I"", ""O"", ""U""], " & _
                        " ""SubObject"": {""NumeroDec"": 0, ""Name"": ""Nome - SubObject"", " & _
                        " ""Array"": null, ""SubObject"": null, ""Valido"": false, ""ID"": 2}, " & _
                        " ""Valido"": true, ""ID"": 1}"

Dim objprova As Prova
objprova = PW.JSON.JSONHelper.StringToObject(strJSON,
GetType(Prova))

Console.WriteLine(objprova.Name)
Console.WriteLine(objprova.SubObject.Name)
Console.ReadLine()
 
End Sub

Il risultato ottenuto a video sarà il seguente:

Nome Object
Nome - SubObject

Ho utilizzato il metodo StringToObject della classe statica PW.JSON.JSONHelper per creare un oggetto di tipo Prova partendo dalla stringa JSON dell'esempio precedente.

A riprova del fatto che l'oggetto è stato creato correttamente con anche le proprietà complesse come SubObject (che è dello stesso tipo dell'oggetto proncipale), vediamo che a video vengono correttamente mostrati i valori delle prorietà Name.

Se desiderate visionare il codice sorgente completo della libreria o scaricare la versione compilata dovete essere utenti registrati e cliccare qui.